﻿/*
===============================================================================   
    Lavenders
    Description: Validation Library for KnockoutJS                             
===============================================================================
*/

(function(factory) {

    //#region Factory required
    'use strict';
    if (typeof window.require === 'function' && typeof window.exports === 'object' && typeof window.module === 'object') {
        factory(window.require('knockout'), window.exports);
    } else if (typeof window.define === 'function' && window.define['amd']) {
        window.define(['knockout', 'exports'], factory);
    } else {
        factory(ko, ko.validation = { });
    }
    //#endregion

}(function(ko, exports) {

    //#region Variables
    if (typeof(ko) === undefined) {
        throw 'Knockout is required';
    }

    var validation = exports;
    ko.validation = validation;

    var defaults = {
        registerExtenders: true,
        messagesOnModified: true,
        errorsAsTitle: true,
        errorsAsTitleOnModified: false,
        messageTemplate: null,
        insertMessages: true,
        parseInputAttributes: false,
        writeInputAttributes: false,
        decorateElement: false,
        errorClass: null,
        errorElementClass: 'validationElement',
        errorMessageClass: 'validationMessage',
        grouping: {
            deep: false,
            observable: true
        }
    };

    var configuration = ko.utils.extend({ }, defaults);
    var html5Attributes = ['required', 'pattern', 'min', 'max', 'step'];
    var html5InputTypes = ['email', 'number', 'date'];

    var async = function(expr) {
        if (window.setImmediate) {
            window.setImmediate(expr);
        } else {
            window.setTimeout(expr, 0);
        }
    };
    //#endregion

    //#region Utilities

    var utils = (function() {
        var seedId = new Date().getTime();

        var domData = { };
        var domDataKey = '__ko_validation__';

        return {
            isArray: function(o) {
                return o.isArray || Object.prototype.toString.call(o) === '[object Array]';
            },
            isObject: function(o) {
                return o !== null && typeof o === 'object';
            },
            values: function(o) {
                var r = [];
                for (var i in o) {
                    if (o.hasOwnProperty(i)) {
                        r.push(o[i]);
                    }
                }
                return r;
            },
            getValue: function(o) {
                return (typeof o === 'function' ? o() : o);
            },
            hasAttribute: function(node, attr) {
                return node.getAttribute(attr) !== null;
            },
            getAttribute: function(element, attr) {
                return element.getAttribute(attr);
            },
            setAttribute: function(element, attr, value) {
                return element.setAttribute(attr, value);
            },
            isValidatable: function(o) {
                return o && o.rules && o.isValid && o.isModified;
            },
            insertAfter: function(node, newNode) {
                node.parentNode.insertBefore(newNode, node.nextSibling);
            },
            newId: function() {
                return seedId += 1;
            },
            getConfigOptions: function(element) {
                var options = utils.contextFor(element);

                return options || configuration;
            },
            setDomData: function(node, data) {
                var key = node[domDataKey];

                if (!key) {
                    node[domDataKey] = key = utils.newId();
                }

                domData[key] = data;
            },
            getDomData: function(node) {
                var key = node[domDataKey];

                if (!key) {
                    return undefined;
                }

                return domData[key];
            },
            contextFor: function(node) {
                switch (node.nodeType) {
                case 1:
                case 8:
                    var context = utils.getDomData(node);
                    if (context) {
                        return context;
                    }
                    if (node.parentNode) {
                        return utils.contextFor(node.parentNode);
                    }
                    break;
                }
                return undefined;
            },
            isEmptyVal: function(val) {
                if (val === undefined) {
                    return true;
                }
                if (val === null) {
                    return true;
                }
                if (val === '') {
                    return true;
                }
                return false;
            },
            getOriginalElementTitle: function(element) {
                var savedOriginalTitle = utils.getAttribute(element, 'data-orig-title'),
                    currentTitle = element.title,
                    hasSavedOriginalTitle = utils.hasAttribute(element, 'data-orig-title');

                return hasSavedOriginalTitle ?
                    savedOriginalTitle : currentTitle;
            }
        };
    }());

    //#endregion

    //#region Public API
    var api = (function() {

        var isInitialized = 0;

        return {
            utils: utils,
            init: function(options, force) {
                if (isInitialized > 0 && !force) {
                    return;
                }
                options = options || { };
                options.errorElementClass = options.errorElementClass || options.errorClass || configuration.errorElementClass;
                options.errorMessageClass = options.errorMessageClass || options.errorClass || configuration.errorMessageClass;
                ko.utils.extend(configuration, options);
                if (configuration.registerExtenders) {
                    exports.registerExtenders();
                }

                isInitialized = 1;
            },

            configure: function(options) { exports.init(options); },
            reset: function() { configuration = jQuery.extend(configuration, defaults); },

            group: function group(obj, options) {
                options = ko.utils.extend(ko.utils.extend({ }, configuration.grouping), options);
                var validatables = ko.observableArray([]),
                    result,
                    traverse = function traverse(obj, level) {
                        var objValues = [],
                            val = ko.utils.unwrapObservable(obj);
                        level = (level !== undefined ? level : options.deep ? 1 : -1);
                        if (ko.isObservable(obj)) {
                            if (!obj.isValid) {
                                obj.extend({ validatable: true });
                            }
                            validatables.push(obj);
                        }
                        if (val) {
                            if (utils.isArray(val)) {
                                objValues = val;
                            } else if (utils.isObject(val)) {
                                objValues = utils.values(val);
                            }
                        }
                        if (level !== 0) {
                            ko.utils.arrayForEach(objValues, function(observable) {
                                if (observable && !observable.nodeType) {
                                    traverse(observable, level + 1);
                                }
                            });
                        }
                    };
                result = null;
                if (options.observable) {

                    traverse(obj);

                    result = ko.computed(function() {
                        var errors = [];
                        ko.utils.arrayForEach(validatables(), function(observable) {
                            if (!observable.isValid()) {
                                errors.push(observable.error);
                            }
                        });
                        return errors;
                    });

                } else {
                    result = function() {
                        var errors = [];
                        validatables([]);
                        traverse(obj);
                        ko.utils.arrayForEach(validatables(), function(observable) {
                            if (!observable.isValid()) {
                                errors.push(observable.error);
                            }
                        });
                        return errors;
                    };
                }

                result.showAllMessages = function(show) {
                    if (show === undefined) {
                        show = true;
                    }

                    result();
                    ko.utils.arrayForEach(validatables(), function(observable) {
                        observable.isModified(show);
                    });
                };

                obj.errors = result;
                obj.isValid = function() {
                    return obj.errors().length === 0;
                };
                obj.isAnyMessageShown = function() {
                    var invalidAndModifiedPresent = false;
                    result();
                    ko.utils.arrayForEach(validatables(), function(observable) {
                        if (!observable.isValid() && observable.isModified()) {
                            invalidAndModifiedPresent = true;
                        }
                    });
                    return invalidAndModifiedPresent;
                };
                return result;
            },

            formatMessage: function(message, params) {
                if (typeof(message) === 'function') {
                    return message(params);
                }
                return message.replace(/\{0\}/gi, ko.utils.unwrapObservable(params));
            },
            
            // todo add a rule
            addRule: function(observable, rule) {
                observable.extend({ validatable: true });
                observable.rules.push(rule);
                return observable;
            },

            //todo add anonymous rule
            addAnonymousRule: function(observable, ruleObj) {
                var ruleName = utils.newId();
                if (ruleObj['message'] === undefined) {
                    ruleObj['message'] = 'Error';
                }

                exports.rules[ruleName] = ruleObj;
                exports.addRule(observable, {
                    rule: ruleName,
                    params: ruleObj.params
                });
            },

            // todo add a extender
            addExtender: function(ruleName) {
                ko.extenders[ruleName] = function(observable, params) {
                    if (params.message || params.onlyIf) {
                        return exports.addRule(observable, {
                            rule: ruleName,
                            message: params.message,
                            params: utils.isEmptyVal(params.params) ? true : params.params,
                            condition: params.onlyIf
                        });
                    } else {
                        return exports.addRule(observable, {
                            rule: ruleName,
                            params: params
                        });
                    }
                };
            },

            // todo register extenders
            registerExtenders: function() {
                if (configuration.registerExtenders) {
                    for (var ruleName in exports.rules) {
                        if (exports.rules.hasOwnProperty(ruleName)) {
                            if (!ko.extenders[ruleName]) {
                                exports.addExtender(ruleName);
                            }
                        }
                    }
                }
            },
            
            // todo create span tag to show error message
            insertValidationMessage: function(element) {

                var span = document.createElement('span');
                span.className = utils.getConfigOptions(element).errorMessageClass;
                utils.insertAfter(element, span);
                return span;
            },
            parseInputValidationAttributes: function(element, valueAccessor) {
                ko.utils.arrayForEach(html5Attributes, function(attr) {
                    if (utils.hasAttribute(element, attr)) {
                        exports.addRule(valueAccessor(), {
                            rule: attr,
                            params: element.getAttribute(attr) || true
                        });
                    }
                });

                var currentType = element.getAttribute('type');
                ko.utils.arrayForEach(html5InputTypes, function(type) {
                    if (type === currentType) {
                        exports.addRule(valueAccessor(), {
                            rule: (type === 'date') ? 'dateISO' : type,
                            params: true
                        });
                    }
                });
            },

            writeInputValidationAttributes: function(element, valueAccessor) {
                var observable = valueAccessor();

                if (!observable || !observable.rules) {
                    return;
                }

                var contexts = observable.rules();

                ko.utils.arrayForEach(html5Attributes, function(attr) {
                    var params;
                    var ctx = ko.utils.arrayFirst(contexts, function(ctx) {
                        return ctx.rule.toLowerCase() === attr.toLowerCase();
                    });

                    if (!ctx) {
                        return;
                    }

                    params = ctx.params;

                    if (ctx.rule === 'pattern') {
                        if (ctx.params instanceof RegExp) {
                            params = ctx.params.source;
                        }
                    }

                    element.setAttribute(attr, params);
                });

                contexts = null;
            },
            // todo take an existing binding handler and make it cause automatic validations
            makeBindingHandlerValidatable: function(handlerName) {
                var init = ko.bindingHandlers[handlerName].init;
                ko.bindingHandlers[handlerName].init = function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
                    init(element, valueAccessor, allBindingsAccessor);
                    return ko.bindingHandlers['validationCore'].init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
                };
            }
        };
    }());

    // todo expose api publicly
    ko.utils.extend(validation, api);
    //#endregion

    //#region Core Validation Rules

    validation.rules = { };
    validation.rules['required'] = {
        validator: function(val, required) {
            var stringTrimRegEx = /^\s+|\s+$/g,
                testVal;

            if (val === undefined || val === null) {
                return !required;
            }

            testVal = val;
            if (typeof(val) === 'string') {
                testVal = val.replace(stringTrimRegEx, '');
            }

            if (!required) {
                return true;
            }

            return ((testVal + '').length > 0);
        },
        message: 'This field is required.'
    };

    validation.rules['min'] = {
        validator: function(val, min) {
            return utils.isEmptyVal(val) || val >= min;
        },
        message: 'Please enter a value greater than or equal to {0}.'
    };

    validation.rules['max'] = {
        validator: function(val, max) {
            return utils.isEmptyVal(val) || val <= max;
        },
        message: 'Please enter a value less than or equal to {0}.'
    };

    validation.rules['minLength'] = {
        validator: function(val, minLength) {
            return utils.isEmptyVal(val) || val.length >= minLength;
        },
        message: 'Please enter at least {0} characters.'
    };

    validation.rules['maxLength'] = {
        validator: function(val, maxLength) {
            return utils.isEmptyVal(val) || val.length <= maxLength;
        },
        message: 'Please enter no more than {0} characters.'
    };

    validation.rules['pattern'] = {
        validator: function(val, regex) {
            return utils.isEmptyVal(val) || val.toString().match(regex) !== null;
        },
        message: 'Please check this value.'
    };

    validation.rules['step'] = {
        validator: function(val, step) {

            // in order to handle steps of .1 & .01 etc.. Modulus won't work
            // if the value is a decimal, so we have to correct for that
            return utils.isEmptyVal(val) || (val * 100) % (step * 100) === 0;
        },
        message: 'The value must increment by {0}'
    };

    validation.rules['email'] = {
        validator: function(val, validate) {
            if (!validate) {
                return true;
            }

            //I think an empty email address is also a valid entry
            //if one want's to enforce entry it should be done with 'required: true'
            return utils.isEmptyVal(val) || (
                // jquery validate regex - thanks Scott Gonzalez
                validate && /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(val)
            );
        },
        message: 'Please enter a proper email address'
    };

    validation.rules['date'] = {
        validator: function(value, validate) {
            if (!validate) {
                return true;
            }
            return utils.isEmptyVal(value) || (validate && !/Invalid|NaN/.test(new Date(value)));
        },
        message: 'Please enter a proper date'
    };

    validation.rules['dateISO'] = {
        validator: function(value, validate) {
            if (!validate) {
                return true;
            }
            return utils.isEmptyVal(value) || (validate && /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value));
        },
        message: 'Please enter a proper date'
    };

    validation.rules['number'] = {
        validator: function(value, validate) {
            if (!validate) {
                return true;
            }
            return utils.isEmptyVal(value) || (validate && /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(value));
        },
        message: 'Please enter a number'
    };

    validation.rules['digit'] = {
        validator: function(value, validate) {
            if (!validate) {
                return true;
            }
            return utils.isEmptyVal(value) || (validate && /^\d+$/.test(value));
        },
        message: 'Please enter a digit'
    };

    validation.rules['phoneUS'] = {
        validator: function(phoneNumber, validate) {
            if (!validate) {
                return true;
            }
            if (typeof(phoneNumber) !== 'string') {
                return false;
            }
            if (utils.isEmptyVal(phoneNumber)) {
                return true;
            }
            phoneNumber = phoneNumber.replace(/\s+/g, '');
            return validate && phoneNumber.length > 9 && phoneNumber.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/);
        },
        message: 'Please specify a valid phone number'
    };

    validation.rules['equal'] = {
        validator: function(val, params) {
            var otherValue = params;
            return val === utils.getValue(otherValue);
        },
        message: 'Values must equal'
    };

    validation.rules['notEqual'] = {
        validator: function(val, params) {
            var otherValue = params;
            return val !== utils.getValue(otherValue);
        },
        message: 'Please choose another value.'
    };

    validation.rules['unique'] = {
        validator: function(val, options) {
            var c = utils.getValue(options.collection),
                external = utils.getValue(options.externalValue),
                counter = 0;

            if (!val || !c) {
                return true;
            }

            ko.utils.arrayFilter(ko.utils.unwrapObservable(c), function(item) {
                if (val === (options.valueAccessor ? options.valueAccessor(item) : item)) {
                    counter++;
                }
            });

            return counter < (external !== undefined && val !== external ? 1 : 2);
        },
        message: 'Please make sure the value is unique.'
    };

    (function() {
        validation.registerExtenders();
    }());

    //#endregion

    //#region Knockout Binding Handlers

    ko.bindingHandlers['validationCore'] = (function() {

        return {
            init: function(element, valueAccessor) {
                var config = utils.getConfigOptions(element);
                if (config.parseInputAttributes) {
                    async(function() { exports.parseInputValidationAttributes(element, valueAccessor); });
                }
                if (config.insertMessages && utils.isValidatable(valueAccessor())) {
                    var validationMessageElement = exports.insertValidationMessage(element);
                    if (config.messageTemplate) {
                        ko.renderTemplate(config.messageTemplate, { field: valueAccessor() }, null, validationMessageElement, 'replaceNode');
                    } else {
                        ko.applyBindingsToNode(validationMessageElement, { validationMessage: valueAccessor() });
                    }
                }

                if (config.writeInputAttributes && utils.isValidatable(valueAccessor())) {

                    exports.writeInputValidationAttributes(element, valueAccessor);
                }

                if (config.decorateElement && utils.isValidatable(valueAccessor())) {
                    ko.applyBindingsToNode(element, { validationElement: valueAccessor() });
                }

                //// todo show summary errors        
                var obsv = valueAccessor(),
                    config = utils.getConfigOptions(element),
                    val = ko.utils.unwrapObservable(obsv),
                    msg = null,
                    isModified = false,
                    isValid = false;
                obsv.extend({ validatable: true });
                isModified = obsv.isModified();
                isValid = obsv.isValid();
                // todo create a handler to correctly return an error message
                var errorMsgAccessor = function() {
                    if (!config.messagesOnModified || isModified) {
                        return isValid ? null : obsv.error;
                    } else {
                        return null;
                    }
                };
                // todo toggle visibility on validation messages when validation hasn't been evaluated, or when the object isValid
                var visiblityAccessor = function() {
                    return (!config.messagesOnModified || isModified) ? !isValid : false;
                };
            }
        };

    }());

    // todo override for KO's default 'value' and 'checked' bindings
    api.makeBindingHandlerValidatable('value');
    api.makeBindingHandlerValidatable('checked');


    ko.bindingHandlers['validationMessage'] = {
        // todo individual error message, if modified or post binding
        update: function(element, valueAccessor) {
            var obsv = valueAccessor(),
                config = utils.getConfigOptions(element),
                val = ko.utils.unwrapObservable(obsv),
                msg = null,
                isModified = false,
                isValid = false;

            obsv.extend({ validatable: true });

            isModified = obsv.isModified();
            isValid = obsv.isValid();
            // todo create a handler to correctly return an error message
            var errorMsgAccessor = function() {
                if (!config.messagesOnModified || isModified) {
                    return isValid ? null : obsv.error;
                } else {
                    return null;
                }
            };

            // todo toggle visibility on validation messages when validation hasn't been evaluated, or when the object isValid
            var visiblityAccessor = function() {
                return (!config.messagesOnModified || isModified) ? !isValid : false;
            };

            ko.bindingHandlers.text.update(element, errorMsgAccessor);
            ko.bindingHandlers.visible.update(element, visiblityAccessor);
        }
    };

    //#region others
    ko.bindingHandlers['validationElement'] = {
        update: function(element, valueAccessor) {
            var obsv = valueAccessor(),
                config = utils.getConfigOptions(element),
                val = ko.utils.unwrapObservable(obsv),
                msg = null,
                isModified = false,
                isValid = false;
            obsv.extend({ validatable: true });

            isModified = obsv.isModified();
            isValid = obsv.isValid();

            var cssSettingsAccessor = function() {
                var css = { };

                var shouldShow = (isModified ? !isValid : false);

                if (!config.decorateElement) {
                    shouldShow = false;
                }

                css[config.errorElementClass] = shouldShow;
                return css;
            };

            // todo add or remove class on the element;
            ko.bindingHandlers.css.update(element, cssSettingsAccessor);
            if (!config.errorsAsTitle) {
                return;
            }

            var origTitle = utils.getAttribute(element, 'data-orig-title'),
                elementTitle = element.title,
                titleIsErrorMsg = utils.getAttribute(element, 'data-orig-title') === 'true';

            var errorMsgTitleAccessor = function() {
                if (!config.errorsAsTitleOnModified || isModified) {
                    if (!isValid) {
                        return { title: obsv.error, 'data-orig-title': utils.getOriginalElementTitle(element) };
                    } else {
                        return { title: utils.getOriginalElementTitle(element), 'data-orig-title': null };
                    }
                }
                return null;
            };
            ko.bindingHandlers.attr.update(element, errorMsgTitleAccessor);
        }
    };

    // todo this binding handler allows you to override the initial config by setting any of the options for a specific element or context of elements
    ko.bindingHandlers['validationOptions'] = (function() {
        return {
            init: function(element, valueAccessor) {
                var options = ko.utils.unwrapObservable(valueAccessor());
                if (options) {
                    var newConfig = ko.utils.extend({ }, configuration);
                    ko.utils.extend(newConfig, options);
                    utils.setDomData(element, newConfig);
                }
            }
        };
    }());
    //#endregion

    //#region Knockout Extenders
    ko.extenders['validation'] = function(observable, rules) {
        ko.utils.arrayForEach(utils.isArray(rules) ? rules : [rules], function(rule) {
            exports.addAnonymousRule(observable, rule);
        });
        return observable;
    };

    ko.extenders['validatable'] = function(observable, enable) {
        if (enable && !utils.isValidatable(observable)) {

            observable.error = ko.observable(null);
            observable.rules = ko.observableArray();
            observable.isValidating = ko.observable(false);
            observable.__valid__ = ko.observable(true);
            observable.isModified = ko.observable(false);

            var h_obsValidationTrigger = ko.computed(function() {
                var obs = observable(),
                    ruleContexts = observable.rules();

                exports.validateObservable(observable);

                return true;
            });

            observable.isValid = ko.computed(function() {
                return observable.__valid__();
            });

            var h_change = observable.subscribe(function() {
                observable.isModified(true);
            });

            observable._disposeValidation = function() {
                observable.isValid.dispose();
                observable.rules.removeAll();
                observable.isModified._subscriptions['change'] = [];
                observable.isValidating._subscriptions['change'] = [];
                observable.__valid__._subscriptions['change'] = [];
                h_change.dispose();
                h_obsValidationTrigger.dispose();

                delete observable['rules'];
                delete observable['error'];
                delete observable['isValid'];
                delete observable['isValidating'];
                delete observable['__valid__'];
                delete observable['isModified'];
            };
        } else if (enable === false && utils.isValidatable(observable)) {

            if (observable._disposeValidation) {
                observable._disposeValidation();
            }
        }
        return observable;
    };

    function validateSync(observable, rule, ctx) {
        // todo Execute the validator and see if its valid
        if (!rule.validator(observable(), ctx.params === undefined ? true : ctx.params)) { // default param is true, eg. required = true

            //todo not valid, so format the error message and stick it in the 'error' variable
            observable.error(exports.formatMessage(ctx.message || rule.message, ctx.params));
            observable.__valid__(false);
            return false;
        } else {
            return true;
        }
    }

    function validateAsync(observable, rule, ctx) {
        observable.isValidating(true);

        var callBack = function(valObj) {
            var isValid = false,
                msg = '';

            if (!observable.__valid__()) {
                observable.isValidating(false);
                return;
            }

            if (valObj['message']) {
                isValid = valObj.isValid;
                msg = valObj.message;
            } else {
                isValid = valObj;
            }

            if (!isValid) {
                observable.error(exports.formatMessage(msg || ctx.message || rule.message, ctx.params));
                observable.__valid__(isValid);
            }
            observable.isValidating(false);
        };
        rule.validator(observable(), ctx.params || true, callBack);
    }

    validation.validateObservable = function(observable) {
        var i = 0,
            rule,
            ctx,
            ruleContexts = observable.rules(),
            len = ruleContexts.length;

        for (; i < len; i++) {

            ctx = ruleContexts[i];
            if (ctx.condition && !ctx.condition()) {
                continue;
            }

            rule = exports.rules[ctx.rule];

            if (rule['async'] || ctx['async']) {
                validateAsync(observable, rule, ctx);

            } else {
                if (!validateSync(observable, rule, ctx)) {
                    return false;
                }
            }
        }

        observable.error(null);
        observable.__valid__(true);
        return true;
    };

    //#endregion

    //#region Validated Observable

    ko.validatedObservable = function(initialValue) {
        if (!exports.utils.isObject(initialValue)) {
            return ko.observable(initialValue).extend({ validatable: true });
        }

        var obsv = ko.observable(initialValue);
        obsv.errors = exports.group(initialValue);
        obsv.ids = initialValue;
        obsv.isValid = ko.computed({
            read: function() {
                return obsv.errors().length === 0;
            },
            write: function(val) {
                return val;
            }
        });

        return obsv;
    };

    //#endregion

    //#region Localization

    // todo quick function to override rule messages
    validation.localize = function(msgTranslations) {

        var msg, rule;
        for (rule in msgTranslations) {
            if (exports.rules.hasOwnProperty(rule)) {
                exports.rules[rule].message = msgTranslations[rule];
            }
        }
    };
    //#endregion

    //#region ApplyBindings Added Functionality
    ko.applyBindingsWithValidation = function(viewModel, rootNode, options) {
        var len = arguments.length,
            node, config;

        if (len > 2) {
            node = rootNode;
            config = options;
        } else if (len < 2) {
            node = document.body;
        } else {
            if (arguments[1].nodeType) {
                node = rootNode;
            } else {
                config = arguments[1];
            }
        }

        exports.init();

        if (config) {
            exports.utils.setDomData(node, config);
        }

        ko.applyBindings(viewModel, rootNode);
    };

    // todo override the original applyBindings so that we can ensure all new rules and what not are correctly registered
    var origApplyBindings = ko.applyBindings;
    ko.applyBindings = function(viewModel, rootNode) {

        exports.init();

        origApplyBindings(viewModel, rootNode);
    };
    //#endregion

    //#region Create binding Summary errors
    // todo create validationSummary data binding

    function ErrorModel(data) {
        var self = this;
        data = data || { };

        self.message = ko.observable(data.message);
        self.element = ko.observable(data.element);

        self.focusElement = function() {
            $(self.element()).focus();
        };
        return self;
    }

    ko.bindingHandlers.validationSummary = {
        init: function(element, valueAccessor, allBindingsAccessor, viewModel) {

            if (valueAccessor()) {

                viewModel.validationModel.errors = viewModel.validationModel.errors || ko.validatedObservable();
                var ids = Object.keys(viewModel.validationModel.ids);
                viewModel.errors = viewModel.errors || ko.observableArray();
                viewModel.validationModel = viewModel.validationModel || ko.validatedObservable();
                viewModel.isStarted = viewModel.isStarted || ko.observable(true);
                var mapped = ko.computed(function() {
                    if (!viewModel.validationModel.isValid()) {
                        if (!viewModel.isStarted()) {
                            $(element).show();
                        } else {
                            $(element).hide();
                            viewModel.isStarted(false);
                        }
                    }
                    return $.map(viewModel.validationModel.errors(), function(item, index) {
                        return new ErrorModel({
                            message: item(),
                            // todo something
                            // element: '#' + ids[index]
                            element: null
                        });
                    });
                });
                viewModel.errors(mapped);
            }
        }
    };
    //#endregion
//#endregion
}));